package io.vividcode.happyride.tripservice.domain;

import io.eventuate.tram.events.aggregates.ResultWithDomainEvents;
import io.vividcode.happyride.common.BaseEntityWithGeneratedId;
import io.vividcode.happyride.common.Position;
import io.vividcode.happyride.common.PositionVO;
import io.vividcode.happyride.tripservice.api.TripState;
import io.vividcode.happyride.tripservice.api.events.CancellationParty;
import io.vividcode.happyride.tripservice.api.events.TripCancellationResolutionRequiredEvent;
import io.vividcode.happyride.tripservice.api.events.TripCancelledEvent;
import io.vividcode.happyride.tripservice.api.events.TripConfirmedEvent;
import io.vividcode.happyride.tripservice.api.events.TripCreatedEvent;
import io.vividcode.happyride.tripservice.api.events.TripDetails;
import io.vividcode.happyride.tripservice.api.events.TripDomainEvent;
import io.vividcode.happyride.tripservice.api.events.TripFinishedEvent;
import io.vividcode.happyride.tripservice.api.events.TripRejectedEvent;
import io.vividcode.happyride.tripservice.api.events.TripStartedEvent;
import io.vividcode.happyride.tripservice.api.web.TripVO;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

import jakarta.persistence.AttributeOverride;
import jakarta.persistence.AttributeOverrides;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Table;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Entity
@Table(name = "trips")
@Getter
@Setter
@NoArgsConstructor
@ToString
public class Trip extends BaseEntityWithGeneratedId {

    public static ResultWithDomainEvents<Trip, TripDomainEvent> createTrip(
            final String passengerId,
            final PositionVO startPos, final PositionVO endPos) {
        final Trip trip = new Trip(passengerId, startPos, endPos);
        final TripCreatedEvent event = new TripCreatedEvent(
                new TripDetails(passengerId, startPos, endPos));
        return new ResultWithDomainEvents<>(trip, event);
    }

    @Column(name = "passenger_id")
    @Size(max = 36)
    private String passengerId;

    @Column(name = "driver_id")
    @Size(max = 36)
    private String driverId;

    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "lat", column = @Column(name = "start_pos_lat")),
            @AttributeOverride(name = "lng", column = @Column(name = "start_pos_lng")),
            @AttributeOverride(name = "addressId", column = @Column(name = "start_pos_address_id"))
    })
    private Position startPos;

    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "lat", column = @Column(name = "end_pos_lat")),
            @AttributeOverride(name = "lng", column = @Column(name = "end_pos_lng")),
            @AttributeOverride(name = "addressId", column = @Column(name = "end_pos_address_id"))
    })
    private Position endPos;

    @Column(name = "fare")
    private BigDecimal fare;

    @Column(name = "state")
    @Enumerated(EnumType.STRING)
    private TripState state;

    public Trip(
            final String passengerId, final PositionVO startPos,
            final PositionVO endPos) {
        this.passengerId = passengerId;
        this.startPos = startPos.deserialize();
        this.endPos = endPos.deserialize();
        this.state = TripState.CREATED;
    }

    public ResultWithDomainEvents<Trip, TripDomainEvent> markAsDispatched() {
        this.assertTripState(TripState.CONFIRMED);
        this.setState(TripState.DISPATCHED);
        return new ResultWithDomainEvents<>(this);
    }

    public ResultWithDomainEvents<Trip, TripDomainEvent> markAsFailed() {
        this.setState(TripState.FAILED);
        return new ResultWithDomainEvents<>(this);
    }

    public ResultWithDomainEvents<Trip, TripDomainEvent> rejectTrip() {
        this.setState(TripState.REJECTED);
        return new ResultWithDomainEvents<>(this, new TripRejectedEvent());
    }

    public ResultWithDomainEvents<Trip, TripDomainEvent> confirmTrip() {
        this.setState(TripState.CONFIRMED);
        final TripDetails tripDetails = new TripDetails(this.passengerId,
                this.startPos.serialize(),
                this.endPos.serialize());
        return new ResultWithDomainEvents<>(this,
                new TripConfirmedEvent(tripDetails));
    }

    public ResultWithDomainEvents<Trip, TripDomainEvent> acceptByDriver(
            final String driverId) {
        this.setDriverId(driverId);
        this.setState(TripState.ACCEPTED);
        return new ResultWithDomainEvents<>(this);
    }

    public ResultWithDomainEvents<Trip, TripDomainEvent> shouldCancel(
            final CancellationParty initiator) {
        final List<TripDomainEvent> events = new ArrayList<>();
        if (initiator == CancellationParty.DRIVER) {
            if (this.state == TripState.CANCELLED_BY_PASSENGER) {
                this.setState(TripState.CANCELLED);
                events.add(new TripCancelledEvent());
            } else {
                this.setState(TripState.CANCELLED_BY_DRIVER);
            }
        } else {
            if (this.state == TripState.CANCELLED_BY_DRIVER) {
                this.setState(TripState.CANCELLED);
                events.add(new TripCancelledEvent());
            } else {
                this.setState(TripState.CANCELLED_BY_PASSENGER);
            }
        }
        return new ResultWithDomainEvents<>(this,
                events.toArray(new TripDomainEvent[0]));
    }

    public ResultWithDomainEvents<Trip, TripDomainEvent> shouldNotCancel(
            final CancellationParty initiator) {
        final List<TripDomainEvent> events = new ArrayList<>();
        if (initiator == CancellationParty.DRIVER) {
            if (this.state == TripState.CANCELLED_BY_PASSENGER) {
                this.setState(TripState.CANCELLATION_REJECTED_BY_DRIVER);
                events.add(new TripCancellationResolutionRequiredEvent(initiator));
            }
        } else {
            if (this.state == TripState.CANCELLED_BY_DRIVER) {
                this.setState(TripState.CANCELLATION_REJECTED_BY_PASSENGER);
                events.add(new TripCancellationResolutionRequiredEvent(initiator));
            }
        }
        return new ResultWithDomainEvents<>(this,
                events.toArray(new TripDomainEvent[0]));
    }

    public ResultWithDomainEvents<Trip, TripDomainEvent> startTrip() {
        this.assertTripState(TripState.ACCEPTED);
        this.setState(TripState.STARTED);
        return new ResultWithDomainEvents<>(this, new TripStartedEvent());
    }

    public ResultWithDomainEvents<Trip, TripDomainEvent> finishTrip() {
        this.assertTripState(TripState.STARTED);
        this.setState(TripState.FINISHED);
        return new ResultWithDomainEvents<>(this, new TripFinishedEvent());
    }

    private void assertTripState(final TripState requiredState) {
        if (this.state != requiredState) {
            throw new IllegalTripStateException(requiredState);
        }
    }

    public TripVO toTripVO() {
        final TripVO tripVO = new TripVO();
        tripVO.setId(this.getId());
        tripVO.setPassengerId(this.getPassengerId());
        tripVO.setDriverId(this.getDriverId());
        tripVO.setStartPos(this.getStartPos().serialize());
        tripVO.setEndPos(this.getEndPos().serialize());
        tripVO.setState(this.getState().name());
        return tripVO;
    }
}
